package kinectData;

import java.awt.Dimension;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.util.Date;
import java.util.Hashtable;

import javax.media.opengl.GL;
import javax.media.opengl.GL2;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCapabilities;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.GLProfile;
import javax.media.opengl.awt.GLCanvas;
import javax.media.opengl.glu.GLU;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JTextField;
import javax.vecmath.Point3d;

import kinectData.models.AccelerometerData;
import kinectData.models.DataReceiver;
import kinectData.models.Device;
import kinectData.models.Match;
import kinectData.models.Matcher;
import kinectData.models.Receiver;
import kinectData.models.Skeleton;
import kinectData.models.Skeleton.Limb;
import kinectKata.service.AccelerometerDataThread;
import kinectKata.service.DeviceSender;
import kinectKata.service.KinectDataThread;

import org.apache.log4j.Logger;

import com.jogamp.opengl.util.FPSAnimator;

/**
 * Visualiser of Kinect skeleton data and handler of device accelerometer data.
 * 
 * @author Markus L�chtefeld (loechti)
 */
public class KinectView implements GLEventListener, MouseListener, MouseMotionListener, 
      DataReceiver, Receiver<Match> {

	private static final int MAX_SKELETON_IDLE_TIME = 2000;
	private Hashtable<String, AccelerometerView> aViews = new Hashtable<String, AccelerometerView>();
	private Hashtable<String, AccelerometerView> kViews = new Hashtable<String, AccelerometerView>();
	private Hashtable<String, Skeleton> skeletons = new Hashtable<String, Skeleton>();
	private Hashtable<String, Device> devices = new Hashtable<String, Device>();
    private KinectDataThread kdt;
    private AccelerometerDataThread adt;
	private GLU glu;
    private float view_rotx = 0.0f, view_roty = 0.0f;
    private int prevMouseX, prevMouseY;
    private Logger logger = Logger.getLogger("logger");
    private boolean isLogging = false;
    private JFrame loggingToolBox;
    
    // 12 colours to display 12 unique skeletons.
    private double[] colorArray =  {1.0, 0.0, 0.0,
    								0.0, 1.0, 0.0,
    								0.0, 0.0, 1.0,
    								1.0, 1.0, 0.0,
    								1.0, 0.0, 1.0,
    								0.0, 1.0, 1.0,
    								1.0, 1.0, 1.0,
    								0.5, 0.0, 1.0,
    								0.5, 0.5, 0.0,
    								1.0, 0.5, 0.0,
    								0.0, 1.0, 0.5,
    								0.5, 0.5, 0.5};

    private final Limb[] limbs_DrawingOrder = {Limb.FOOTRIGHT, Limb.ANKLERIGHT, 
    							   Limb.ANKLERIGHT, Limb.KNEERIGHT, 
    							   Limb.KNEERIGHT, Limb.HIPRIGHT,
    							   Limb.FOOTLEFT, Limb.ANKLELEFT,
    							   Limb.ANKLELEFT, Limb.KNEELEFT,
    							   Limb.KNEELEFT, Limb.HIPLEFT,
    							   Limb.HIPLEFT, Limb.HIPCENTER,
    							   Limb.HIPCENTER, Limb.HIPRIGHT,
    							   Limb.HIPCENTER, Limb.SPINE,
    							   Limb.SPINE, Limb.SHOULDERCENTER,
    							   Limb.SHOULDERCENTER, Limb.SHOULDERRIGHT,
    							   Limb.SHOULDERCENTER, Limb.SHOULDERLEFT,
    							   Limb.SHOULDERLEFT, Limb.ELBOWLEFT,
    							   Limb.ELBOWLEFT, Limb.WRISTLEFT,
    							   Limb.WRISTLEFT, Limb.HANDLEFT, 
    							   Limb.SHOULDERRIGHT, Limb.ELBOWRIGHT,
    							   Limb.ELBOWRIGHT, Limb.WRISTRIGHT,
    							   Limb.WRISTRIGHT, Limb.HANDRIGHT,
    							   Limb.SHOULDERCENTER, Limb.HEAD};	
    
	public static void main(String[] args) {
		Frame frame = new Frame("Kinect Window");
        GLProfile glp = GLProfile.getDefault();
        GLCapabilities caps = new GLCapabilities(glp);
		GLCanvas canvas = new GLCanvas(caps);
		KinectView kv = new KinectView();
		canvas.addGLEventListener(kv);
		frame.add(canvas);
		frame.setSize(800, 600);
		final FPSAnimator animator = new FPSAnimator(canvas, 60);
		frame.addWindowListener(new WindowAdapter() {
			@Override public void windowClosing(WindowEvent e) {
				new Thread(new Runnable() {
					public void run() {
						animator.stop();
						System.exit(0);
					}
				}).start();
			}
		});
		canvas.addMouseListener(kv);
		canvas.addMouseMotionListener(kv);
		frame.setVisible(true);
		animator.start();
	}

	@Override
	public synchronized void addSkeleton(String data) {
		Skeleton newSkeleton = new Skeleton(data);
		removeOldData();
		
		// Add skeleton.
		String id = String.valueOf(newSkeleton.getTrackingID());
		Skeleton stored = skeletons.get(id);
		if (stored == null) { 
			skeletons.put(id, newSkeleton);
			System.out.println("Skeleton added " + id);
		} else {
			stored.update(data);
			newSkeleton = stored;
		}
		
		// Update the view.
		Point3d limbAcceleration = newSkeleton.getAcceleration(Limb.HANDRIGHT);
		String kinectId = String.valueOf(newSkeleton.getTrackingID());
		AccelerometerView kinectPlot = kViews.get(kinectId.toLowerCase()); 
		if (kinectPlot == null) {
			AccelerometerView newKinectPlot = new AccelerometerView(kinectId, 810, 20 + kViews.size() * 260);
			newKinectPlot.addData(new AccelerometerData(limbAcceleration));
			kViews.put(newKinectPlot.getMac().toLowerCase(), newKinectPlot);
		} else {
			kinectPlot.addData(new AccelerometerData(limbAcceleration));
		}
		
		if (isLogging) {
			logger.info(newSkeleton);
		}
	}

	/**
	 * Log device accelerometer data.
	 */
	@Override
	public synchronized void addSensorData(String deviceId, String ip, int port, Point3d p, 
			String rawData) {
		
		Device newDevice = new Device(ip, port, rawData);
		// Add device.
		Device stored = devices.get(newDevice.getId());
		if (stored == null) {
			devices.put(newDevice.getId(), newDevice);
			System.out.println("Device added " + ip + ":" + port);
		} else {
			stored.update(rawData);
		}
		// Add to the view.
		AccelerometerView devicePlot = aViews.get(deviceId.toLowerCase()); 
		if (devicePlot == null) {
			AccelerometerView newDevicePlot = new AccelerometerView(deviceId, 810, 20 + aViews.size() * 260);
			newDevicePlot.addData(new AccelerometerData(newDevice.getPosition()));
			aViews.put(newDevicePlot.getMac().toLowerCase(), newDevicePlot);
		} else {
			devicePlot.addData(new AccelerometerData(newDevice.getPosition()));
		}
		
		if (isLogging) { 
			logger.info(newDevice);
		}
		if (skeletons.size() > 0) {
			// Device has moved: check if it is in sync with a skeleton.
			// TODO(norrielm): Compare the single device with all skeletons rather than all devices.
			Matcher.compare(devices.values(), skeletons.values(), this);
		}
	}
	
	private void removeOldData(){
		// Remove idle skeletons.
		long currentTime = new Date().getTime();
		
		for (Skeleton s : skeletons.values()) {
			if ((currentTime - s.getTrackingTime()) > MAX_SKELETON_IDLE_TIME) {
				synchronized(skeletons) {
					skeletons.remove(String.valueOf(s.getTrackingID()));
				}
				kViews.get(String.valueOf(s.getTrackingID()).toLowerCase()).closeView();
				synchronized(kViews) {
					kViews.remove(String.valueOf(s.getTrackingID()).toLowerCase());
				}
			}
		}
	}
	
	@Override
	public synchronized void accept(Match match) {
		// TODO(loechti) do something intelligent with the match...
		System.out.println(match);
		
		Skeleton skeleton = skeletons.get(String.valueOf(match.getSkeleton().getTrackingID()));
		Device device = devices.get(match.getDevice().getId());
		
		// TODO(norrielm): Send matched skeleton profile_id to devices.
		if (!skeleton.hasProfileId()) {
			skeleton.setProfileId(device.getId());
			System.out.println("Skeleton profile set to " + device.getId());
		} else {
			System.out.println("Skeleton profile set " + (skeleton.hasProfileId() ? "Y" : "N"));
		}
		DeviceSender.sendToDevice(device.getIp(), device.getPort(), skeleton.getProfileId());
	}	
	
	@Override
	public synchronized void display(GLAutoDrawable drawable) {
		GL2 gl = drawable.getGL().getGL2();
		gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
		gl.glLoadIdentity();
        gl.glRotatef(view_rotx, 1.0f, 0.0f, 0.0f);
        gl.glRotatef(view_roty, 0.0f, 1.0f, 0.0f);

        gl.glBegin(GL.GL_LINES);
    	gl.glColor3f(1.0f, 0.0f, 0.0f);
    		gl.glVertex3f(0.0f,0.0f,0.0f);
    		gl.glVertex3f(1.0f,0.0f,0.0f);
    	gl.glColor3f(0.0f, 1.0f, 0.0f);	
    		gl.glVertex3f(0.0f,0.0f,0.0f);
    		gl.glVertex3f(0.0f,1.0f,0.0f);
    	gl.glColor3f(0.0f, 0.0f, 1.0f);	
    		gl.glVertex3f(0.0f,0.0f,0.0f);
    		gl.glVertex3f(0.0f,0.0f,1.0f);
    	gl.glEnd();
    	
    	int counter = 0;
    	for (Skeleton s : skeletons.values()) {
    		gl.glBegin(GL.GL_LINES);
    		gl.glColor3d(colorArray[counter*3], colorArray[counter*3+1], colorArray[counter*3+2]);
    		// TODO(norrielm): fix so that Skeleton.Limb (i.e. kinect received order) == drawingOrder. 
    		for (Limb limb : limbs_DrawingOrder) {
    			Point3d pos = s.getPosition(limb);
    			if (pos != null) {
    				gl.glVertex3d(pos.x, pos.y, pos.z);
    			}
    		}
    		gl.glEnd();
    		counter++;
    	}
		gl.glFlush();
	}

	@Override
	public void init(GLAutoDrawable drawable) {
        GL2 gl = drawable.getGL().getGL2();
        gl.setSwapInterval(1);
        gl.glShadeModel(GL2.GL_SMOOTH);              
        gl.glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // Black
        gl.glClearDepth(1.0f);
        gl.glEnable(GL.GL_DEPTH_TEST);
        gl.glDepthFunc(GL.GL_LEQUAL);
        gl.glHint(GL2.GL_PERSPECTIVE_CORRECTION_HINT, GL.GL_NICEST);
        startLoggingToolBox();
        kdt = new KinectDataThread(this);
        kdt.start();
        adt = new AccelerometerDataThread(this);
        adt.start();
	}
	
	private void startLoggingToolBox() {
		loggingToolBox = new JFrame("Logging");
		loggingToolBox.setSize(250,80);
		loggingToolBox.setLocation(0, 650);
		loggingToolBox.setLayout(null);
		loggingToolBox.setResizable(false);
		loggingToolBox.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		final JTextField filenameField = new JTextField("logfile");
		filenameField.setSize(130,45);
		filenameField.setLocation(110, 7);
		loggingToolBox.add(filenameField);
		final JButton b = new JButton("Start Log");
		b.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent event) {
				if (b.getText().equalsIgnoreCase("Start Log")) {
					b.setText("Stop Log");
					isLogging = true;
				} else if (b.getText().equalsIgnoreCase("Stop Log")) {
					b.setText("Start Log");
					isLogging = false;
					String loggerFilename = logger.getName() + ".log";
					File loggerFile = new File(loggerFilename);
					String filename = filenameField.getText().trim() + ".log";
					File newLogFile = new File(filename);
					
					// Clear the logfile once saved or warn the user.
					if (loggerFile.renameTo(newLogFile) && loggerFile.exists()) {
						System.out.println("Log saved as " + filename);
						System.out.println("Clearing " + loggerFilename);
						loggerFile.delete();
					} else if (loggerFile.exists()) {
						System.out.println(String.format("Failed to rename '%s' as '%s'", 
												loggerFilename, filename));
						System.out.println(loggerFilename + " will not be cleared.");
					} else {
						System.out.println(String.format("Failed to rename '%s' as '%s'", 
							loggerFilename, filename));
						System.out.println(loggerFilename + " does not exist.");
					}
				}
			}
		});
		b.setSize(100, 50);
		b.setLocation(5,5);
		loggingToolBox.add(b);
		loggingToolBox.setVisible(true);
	}

	@Override
    public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
        GL2 gl = drawable.getGL().getGL2();
        glu = new GLU();

        // It is possible that height may be zero.
        final float h = (float) width / (float) (height <= 0 ? 1 : height);
        gl.glViewport(0, 0, width, height);
        gl.glMatrixMode(GL2.GL_PROJECTION);
        gl.glLoadIdentity();
        glu.gluPerspective(45.0f, h, 0.1, 20.0);
        glu.gluLookAt(1.0,3.0,-3.0,1.0,1.0,1.0,0.0,1.0,0.0);
        gl.glMatrixMode(GL2.GL_MODELVIEW);
        gl.glLoadIdentity();
    }

	@Override
	public void mouseDragged(MouseEvent e) {
        int x = e.getX();
        int y = e.getY();
        Dimension size = e.getComponent().getSize();
        float thetaY = 360.0f * ((float)(x-prevMouseX)/(float)size.width);
        float thetaX = 360.0f * ((float)(prevMouseY-y)/(float)size.height);
        prevMouseX = x;
        prevMouseY = y;
        view_rotx += thetaX;
        view_roty += thetaY;
	}

	@Override
	public void mousePressed(MouseEvent e) {
        prevMouseX = e.getX();
        prevMouseY = e.getY();
	}

	@Override
	public void mouseMoved(MouseEvent unused) {
	}

	@Override
	public void mouseClicked(MouseEvent unused) {	
	}

	@Override
	public void mouseEntered(MouseEvent unused) {	
	}

	@Override
	public void mouseExited(MouseEvent unused) {		
	}

	@Override
	public void mouseReleased(MouseEvent unused) {	
	}

	@Override
	public void dispose(GLAutoDrawable unused) {	
	}
}
